
package org.apache.solr.schema;

import java.io.IOException;
import java.io.Reader;
import java.util.Map;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.Tokenizer;
import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
import org.apache.lucene.index.AtomicReaderContext;
import org.apache.lucene.index.IndexableField;
import org.apache.lucene.queries.function.FunctionValues;
import org.apache.lucene.queries.function.ValueSource;
import org.apache.lucene.queries.function.docvalues.BoolDocValues;
import org.apache.lucene.queries.function.valuesource.OrdFieldSource;
import org.apache.lucene.search.FieldCache;
import org.apache.lucene.search.SortField;
import org.apache.lucene.util.BytesRef;
import org.apache.lucene.util.CharsRef;
import org.apache.lucene.util.mutable.MutableValue;
import org.apache.lucene.util.mutable.MutableValueBool;
import org.apache.solr.analysis.SolrAnalyzer;
import org.apache.solr.response.TextResponseWriter;
import org.apache.solr.search.QParser;

public class BoolField extends PrimitiveFieldType {

    @Override
    public SortField getSortField(SchemaField field, boolean reverse) {
        field.checkSortability();
        return getStringSort(field, reverse);
    }

    @Override
    public ValueSource getValueSource(SchemaField field, QParser qparser) {
        field.checkFieldCacheSource(qparser);
        return new BoolFieldSource(field.name);
    }
    // avoid instantiating every time...
    protected final static char[] TRUE_TOKEN = {'T'};
    protected final static char[] FALSE_TOKEN = {'F'};
    ////////////////////////////////////////////////////////////////////////
    // TODO: look into creating my own queryParser that can more efficiently
    // handle single valued non-text fields (int,bool,etc) if needed.
    protected final static Analyzer boolAnalyzer = new SolrAnalyzer() {
        @Override
        public TokenStreamComponents createComponents(String fieldName, Reader reader) {

            Tokenizer tokenizer = new Tokenizer(reader) {
                final CharTermAttribute termAtt = addAttribute(CharTermAttribute.class);
                boolean done = false;

                @Override
                public void reset() throws IOException {
                    done = false;
                }

                @Override
                public boolean incrementToken() throws IOException {
                    clearAttributes();
                    if(done) {
                        return false;
                    }
                    done = true;
                    int ch = input.read();
                    if(ch == -1) {
                        return false;
                    }
                    termAtt.copyBuffer(((ch == 't' || ch == 'T' || ch == '1') ? TRUE_TOKEN : FALSE_TOKEN), 0, 1);
                    return true;
                }
            };

            return new TokenStreamComponents(tokenizer);
        }
    };

    @Override
    public Analyzer getAnalyzer() {
        return boolAnalyzer;
    }

    @Override
    public Analyzer getQueryAnalyzer() {
        return boolAnalyzer;
    }

    @Override
    public String toInternal(String val) {

        char ch = (val != null && val.length() > 0) ? val.charAt(0) : 0;
        return (ch == '1' || ch == 't' || ch == 'T') ? "T" : "F";
    }

    @Override
    public String toExternal(IndexableField f) {
        return indexedToReadable(f.stringValue());
    }

    @Override
    public Boolean toObject(IndexableField f) {
        return Boolean.valueOf(toExternal(f));
    }

    @Override
    public Object toObject(SchemaField sf, BytesRef term) {
        return term.bytes[term.offset] == 'T';
    }

    @Override
    public String indexedToReadable(String indexedForm) {
        char ch = indexedForm.charAt(0);
        return ch == 'T' ? "true" : "false";
    }
    private static final CharsRef TRUE = new CharsRef("true");
    private static final CharsRef FALSE = new CharsRef("false");

    @Override
    public CharsRef indexedToReadable(BytesRef input, CharsRef charsRef) {

        if(input.length > 0 && input.bytes[input.offset] == 'T') {
            charsRef.copyChars(TRUE);
        }
        else {
            charsRef.copyChars(FALSE);
        }
        return charsRef;
    }

    @Override
    public void write(TextResponseWriter writer, String name, IndexableField f) throws IOException {
        writer.writeBool(name, f.stringValue().charAt(0) == 'T');
    }
}

// TODO - this can be much more efficient - use OpenBitSet or Bits
class BoolFieldSource extends ValueSource {

    protected String field;

    public BoolFieldSource(String field) {
        this.field = field;
    }

    @Override
    public String description() {
        return "bool(" + field + ')';
    }

    @Override
    public FunctionValues getValues(Map context, AtomicReaderContext readerContext) throws IOException {

        final FieldCache.DocTermsIndex sindex = FieldCache.DEFAULT.getTermsIndex(readerContext.reader(), field);

        // figure out what ord maps to true
        int nord = sindex.numOrd();
        BytesRef br = new BytesRef();
        int tord = -1;
        for(int i = 1; i < nord; i++) {
            sindex.lookup(i, br);
            if(br.length == 1 && br.bytes[br.offset] == 'T') {
                tord = i;
                break;
            }
        }

        final int trueOrd = tord;

        return new BoolDocValues(this) {
            @Override
            public boolean boolVal(int doc) {
                return sindex.getOrd(doc) == trueOrd;
            }

            @Override
            public boolean exists(int doc) {
                return sindex.getOrd(doc) != 0;
            }

            @Override
            public ValueFiller getValueFiller() {
                return new ValueFiller() {
                    private final MutableValueBool mval = new MutableValueBool();

                    @Override
                    public MutableValue getValue() {
                        return mval;
                    }

                    @Override
                    public void fillValue(int doc) {
                        int ord = sindex.getOrd(doc);
                        mval.value = (ord == trueOrd);
                        mval.exists = (ord != 0);
                    }
                };
            }
        };
    }

    @Override
    public boolean equals(Object o) {
        return o.getClass() == BoolFieldSource.class && this.field.equals(((BoolFieldSource)o).field);
    }
    private static final int hcode = OrdFieldSource.class.hashCode();

    @Override
    public int hashCode() {
        return hcode + field.hashCode();
    }
}
